/* Implementation of the CairoUI widgets */

#include <sys/time.h>  // gettimeofday() for mcp timer
#include <cairo.h>

#include "mcp.h"
#include "mediainfo.h"
#include "coverart.h"

#include "ui_cairo.h"
#include "uic_widget.h"
#include "uic_widgets.h"

namespace uic
{

/* Determine font metrics for numeric digits and a few other characters,
 * rather than using a font's overall metrics.
 * This checks the spacing of numeric digits, the time separator, and any
 * characters in the others string.
 * These metrics are only for special layout needs, such as centering text
 * on a separator or "fixed width" based on a set of glyphs.
 *
 * // FIXME -  correctly get single utf-8 characters
 * // TODO  -  arbitrary numeric digits
 *          -  obtain/use the locale time separator
 *          -  right-to-left? top to bottom fonts?
 *             ?  are numeric fields right to left, and/or time fields
 */
static void get_font_metrics (cairo_t *cairo, struct font_metric *fm,
                              const char *others)
    {
    int y_bearing;
    cairo_text_extents_t ext;

    cairo_set_font_size(cairo, fm->size);

    cairo_text_extents(cairo, ":", &ext); // FIXME time separator
    fm->timesep_advance = ext.x_advance;
    y_bearing = ext.y_bearing;

    fm->height = ext.height;     /// start with the
    fm->advance = ext.x_advance; //  time sep's values
    for ( int i = 0; others && others[i] != '\0'; i++ )
        {
        char tmp[2] = { others[i], '\0' };
        cairo_text_extents(cairo, tmp, &ext);
        if ( ext.height > fm->height ) fm->height = ext.height;
        if ( ext.x_advance > fm->advance ) fm->advance = ext.x_advance;
        if ( ext.y_bearing < y_bearing ) y_bearing = ext.y_bearing;
        }

    fm->num_advance = 0;
    char *num = (char *)"0123456789";  // FIXME use locale
    for ( int i = 0; num[i] != '\0'; i++ )
        {
        char digit[2] = { num[i], '\0' };
        cairo_text_extents(cairo, digit, &ext);
        if ( ext.height > fm->height ) fm->height = ext.height;
        if ( ext.y_bearing < y_bearing ) y_bearing = ext.y_bearing;
        if ( ext.x_advance > fm->advance ) fm->advance = ext.x_advance;
        if ( ext.x_advance > fm->num_advance )
            {
            fm->num_advance = ext.x_advance;
            fm->widest_digit = num[i];
            }
        }

    fm->y_origin = fm->size - (-ext.y_bearing);
    }

// -   Base Widget Definition                                             >fold>

Widget * Widget::tail ()
    {
    for ( Widget *o = this; ; o = o->next )
        if ( !o->next )
            return o;
    }
    
bool Widget::squarify_small ()
    {
    if ( width > height )
        { width = height; return true; }
    else if ( height > width )
        { height = width; return true; }
    return false;
    }

bool Widget::squarify_big ()
    {
    if ( width < height )
        { height = width; return true; }
    else if ( height > width )
        { width = height; return true; }
    return false;
    }

void Widget::set_timer (int milliseconds)
    {
    gettimeofday(&timer_activate, 0);
    timer_activate.tv_sec += milliseconds / 1000;
    timer_activate.tv_usec += (milliseconds - timer_activate.tv_sec*1000) * 100;
    if ( timer_activate.tv_usec >= 1000000 )
        {
        timer_activate.tv_sec++;
        timer_activate.tv_usec -= 1000000;
        }
    }

void Widget::clear_timer ()
    {
    timer_activate.tv_sec = 0;
    timer_activate.tv_usec = 0;
    }

// ---------------------------------------------------------------------- <fold<
// -   Button Definition                                                  >fold>

void w_Button::check_size (CairoUI *ui)
    {
    // Most buttons render okay when not square, but they then 'look funny.'
    squarify_small();
    }

bool w_Button::button_press (CairoUI *ui, GenericEvent *ev)
    {
    if ( ev->button == LEFT_CLICK )
        {
        pressed = true;
        redraw_needed = true;
        return true;
        }
    return false;
    }

bool w_Button::pointer_motion (CairoUI *ui, GenericEvent *ev)
    {
    if ( mouseover )
        return false;
    mouseover = true;
//    draw(ui);
    redraw_needed = true;
    return true;
    }

void w_Button::pointer_leave (CairoUI *ui)
    {
    if ( !mouseover )
        fprintf(stderr, "w_Button pointerLeave !mouseover OOPS\n");
    mouseover = false;
//    draw(ui);
    redraw_needed = true;
    }

// Draw a basic blank button, setting ??_arg with the remaining available
// drawing area, and return the width of the outside border.
double w_Button::draw_button (CairoUI *ui, cairo_t *cr, 
                              double *x1_arg, double *y1_arg,
                              double *x2_arg, double *y2_arg)
    {
    double border = width / 32;
//    if ( border > 10 ) border = 10;
    if ( border < 1.0 ) border = 1;

    // fill in the default button color
    ui->button_bg.set(cr);
    cairo_set_line_width(cr, 0);
    cairo_rectangle(cr, x1, y1, width, height);
    cairo_fill(cr);

    if ( mouseover )
        {
        cairo_save(cr);
        // Second, fill the interior with a different shade
        // TODO  darken with an alpha fill instead?
        ui->button_bg.set_adjusted(cr, pressed ? -0.2 : 0.2);
        cairo_set_line_width(cr, 0);
        cairo_move_to(cr, x1 + border/2, y1 + border/2);
        cairo_line_to(cr, x2 - border/2, y1 + border/2);
        cairo_line_to(cr, x2 - border/2, y2 - border/2);
        cairo_line_to(cr, x1 + border/2, y2 - border/2);
        cairo_line_to(cr, x1 + border/2, y1 + border/2);
        cairo_fill(cr);

        // Third, Draw the outside of the square using 'light' from upper left
        // (drawing clockwise starting at the bottom left)
        cairo_set_line_width(cr, border / 2);
        cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
        cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);

        if ( pressed )
            ui->button_bg.set_adjusted(cr, -0.5);
        else
            cairo_set_source_rgb(cr, .99, .99, .99);
        cairo_move_to(cr, x1 + border*1/2, y2 - border*1/2);
        cairo_line_to(cr, x1 + border*1/2, y1 + border*1/2);
        cairo_line_to(cr, x2 - border*1/2, y1 + border*1/2);
        cairo_line_to(cr, x2 - border*1/2, y1 + border    );
        cairo_stroke(cr);

        if ( pressed )
            cairo_set_source_rgb(cr, .99, .99, .99);
        else
            cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
        cairo_move_to(cr, x2 - border*1/2, y1 + border    );
        cairo_line_to(cr, x2 - border*1/2, y2 - border*1/2);
        cairo_line_to(cr, x1 + border    , y2 - border*1/2);
        cairo_stroke(cr);

        if ( pressed ) // black inset on top/left
            {
            cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
            cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
            cairo_move_to(cr, x1 + border*3/2, y2 - border - 1);
            cairo_line_to(cr, x1 + border*3/2, y1 + border*3/2);
            cairo_line_to(cr, x2 - border - 1, y1 + border*3/2);
            }
        else // dark inset on right/bottom
            {
            ui->button_bg.set_adjusted(cr, -0.5);
            cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
            cairo_move_to(cr, x2 - border*1/2, y1 + border + 1);
            cairo_line_to(cr, x2 - border*1/2, y2 - border*1/2);
            cairo_line_to(cr, x1 + border + 1, y2 - border*1/2);
            }
        cairo_stroke(cr);
        cairo_restore(cr);
        }

    *x1_arg = x1 + 1.5 * border; *x2_arg = x2 - 1.5 * border;
    *y1_arg = y1 + 1.5 * border; *y2_arg = y2 - 1.5 * border;
    return border;
    }

void w_Button::set_button_gradient (cairo_pattern_t *pattern, CairoUI *ui)
    {
    cairo_pattern_add_color_stop_rgba(pattern, 0.0, ui->button_fg1.red,
             ui->button_fg1.green, ui->button_fg1.blue, ui->button_fg1.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 0.4, ui->button_fg.red,
             ui->button_fg.green, ui->button_fg.blue, ui->button_fg.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 0.6, ui->button_fg.red,
             ui->button_fg.green, ui->button_fg.blue, ui->button_fg.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 1.0, ui->button_fg2.red,
             ui->button_fg2.green, ui->button_fg2.blue, ui->button_fg2.alpha);
    }

// --------------------------------------------------------------------- <fold<
// -   Clickable Buttons                                                 >fold>

void w_Stop::draw (CairoUI *ui, cairo_t *cr)
    {
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);

    // 1/6th space as a border on both sides, 2/3rds space for the stop icon
    // border sizes: (x2-x1)/6, (y2-y1)/6
    use_x1 = use_x1 + (use_x2-use_x1)/6;  use_x2 = use_x2 - (use_x2-use_x1)/6;
    use_y1 = use_y1 + (use_y2-use_y1)/6;  use_y2 = use_y2 - (use_y2-use_y1)/6;
    
    cairo_pattern_t *pattern;
    pattern = cairo_pattern_create_linear(x1+x1*1/5, y1, x2-x2*1/5, y2);
    set_button_gradient(pattern, ui);
    cairo_rectangle(cr, use_x1, use_y1, use_x2-use_x1, use_y2-use_y1);
    cairo_set_source(cr, pattern);
    cairo_fill(cr);
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

bool w_Stop::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            mcp->command(COMMAND_STOP);
        return true;
        }
    return false;
    }

// ---------------------------------------------------------------------- //

bool w_Play::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            mcp->command(COMMAND_PLAY);
        return true;
        }
    return false;
    }

void w_Play::draw (CairoUI *ui, cairo_t *cr)
    {
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);
    use_x1 += (use_x2-use_x1)/6;  use_x2 -= (use_x2-use_x1)/6;
    use_y1 += (use_y2-use_y1)/8;  use_y2 -= (use_y2-use_y1)/8;

    cairo_set_line_width(cr, 0);

    cairo_move_to(cr, use_x1, use_y1);
    cairo_line_to(cr, use_x1, use_y2);
    cairo_line_to(cr, use_x2, use_y1 + (use_y2-use_y1)/2);
    cairo_line_to(cr, use_x1, use_y1);

    cairo_pattern_t *pattern = cairo_pattern_create_linear(
            use_x1, use_y1+use_y1/5, use_x2, use_y2-use_y1/5);
    set_button_gradient(pattern, ui);
    cairo_set_source(cr, pattern);

    cairo_fill(cr);
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

// ---------------------------------------------------------------------- //

bool w_Pause::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            mcp->command(COMMAND_PAUSE);
        return true;
        }
    return false;
    }

void w_Pause::draw (CairoUI *ui, cairo_t *cr)
    {
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);
    use_x1 += (use_x2-use_x1)/6;  use_x2 -= (use_x2-use_x1)/6;
    use_y1 += (use_y2-use_y1)/6;  use_y2 -= (use_y2-use_y1)/6;

    cairo_set_line_width(cr, 0);

    double linewidth = (use_x2 - use_x1) / 3;
//        double cx = use_x1 + (use_x2 - use_x1) / 2;
    cairo_rectangle(cr, use_x1, use_y1, linewidth, use_y2-use_y1);
    cairo_rectangle(cr, use_x2-linewidth, use_y1,
                               linewidth, use_y2-use_y1);

    cairo_pattern_t *pattern = cairo_pattern_create_linear(
            use_x1, use_y1+use_y1/5, use_x2, use_y2-use_y1/5);
    set_button_gradient(pattern, ui);
    cairo_set_source(cr, pattern);

    cairo_fill(cr);
    cairo_set_source_rgb(cr, 0,0,0);  // clear the pattern right away
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

// ---------------------------------------------------------------------- //

void w_PlayPause::event (CairoUI *ui, Event ev)
    {
    if ( ev == EVENT_PLAYING || ev == EVENT_PAUSED )
        redraw_needed = true;
    }

bool w_PlayPause::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            {
            if ( mcp->playing() )
                mcp->command(COMMAND_PAUSE);
            else
                mcp->command(COMMAND_PLAY);
            }
        return true;
        }
    return false;
    }

void w_PlayPause::draw (CairoUI *ui, cairo_t *cr)
    {
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);

    cairo_set_line_width(cr, 0);

    if ( mcp->playing() )  // draw a pause symbol
        {
        use_x1 += (use_x2-use_x1)/6;  use_x2 -= (use_x2-use_x1)/6;
        use_y1 += (use_y2-use_y1)/6;  use_y2 -= (use_y2-use_y1)/6;
        double linewidth = (use_x2 - use_x1) / 3;
//        double cx = use_x1 + (use_x2 - use_x1) / 2;
        cairo_rectangle(cr, use_x1, use_y1, linewidth, use_y2-use_y1);
        cairo_rectangle(cr, use_x2-linewidth, use_y1,
                                   linewidth, use_y2-use_y1);
        }
    else  // paused or stopped, draw the play symbol
        {
        use_x1 += (use_x2-use_x1)/6;  use_x2 -= (use_x2-use_x1)/6;
        use_y1 += (use_y2-use_y1)/8;  use_y2 -= (use_y2-use_y1)/8;
        cairo_move_to(cr, use_x1, use_y1);
        cairo_line_to(cr, use_x1, use_y2);
        cairo_line_to(cr, use_x2, use_y1 + (use_y2-use_y1)/2);
        cairo_line_to(cr, use_x1, use_y1);
        }

    cairo_pattern_t *pattern = cairo_pattern_create_linear(
            use_x1, use_y1+use_y1/5, use_x2, use_y2-use_y1/5);
    set_button_gradient(pattern, ui);
    cairo_set_source(cr, pattern);

    cairo_fill(cr);
    cairo_set_source_rgb(cr, 0,0,0);  // clear the pattern right away
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

// ---------------------------------------------------------------------- //

static void plotNextIcon (cairo_t *cr, double x, double y,
                                       double width, double height)
    {
    cairo_move_to(cr, x, y);
    cairo_line_to(cr, x, y + height);
    cairo_line_to(cr, x + width*5/8, y + height/2);
    cairo_line_to(cr, x, y);
    
    cairo_rectangle(cr, x + width*6/8, y, width*1/8, height);
    }

bool w_Previous::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            mcp->command(COMMAND_PREVIOUS);
        return true;
        }
    return false;
    }

void w_Previous::draw (CairoUI *ui, cairo_t *cr)
    {
    cairo_pattern_t *pattern;
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);

    // usual button icon spacing
    use_x1 = use_x1 + (use_x2-use_x1)/6;  use_x2 = use_x2 - (use_x2-use_x1)/6;
    use_y1 = use_y1 + (use_y2-use_y1)/6;  use_y2 = use_y2 - (use_y2-use_y1)/6;

    pattern = cairo_pattern_create_linear(x1+x1*1/5, y1, x2-x2*1/5, y2);
    set_button_gradient(pattern, ui);

    cairo_set_line_width(cr, 0);
    cairo_set_source(cr, pattern);
    cairo_save(cr);
    cairo_translate(cr, use_x2, use_y2);
    cairo_rotate(cr, M_PI);
    plotNextIcon(cr, 0, 0, use_x2-use_x1, use_y2-use_y1);
    cairo_restore(cr);
    cairo_fill(cr);

    cairo_set_source_rgb(cr, 0,0,0);  // clear the pattern right away
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

bool w_Next::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        redraw_needed = true;
        if ( inside )
            mcp->command(COMMAND_NEXT);
        return true;
        }
    return false;
    }

void w_Next::draw (CairoUI *ui, cairo_t *cr)
    {
    cairo_pattern_t *pattern;
    double use_x1, use_y1,
           use_x2, use_y2;

    draw_button(ui, cr, &use_x1, &use_y1, &use_x2, &use_y2);

    // usual button icon spacing
    use_x1 = use_x1 + (use_x2-use_x1)/6;  use_x2 = use_x2 - (use_x2-use_x1)/6;
    use_y1 = use_y1 + (use_y2-use_y1)/6;  use_y2 = use_y2 - (use_y2-use_y1)/6;
    
    pattern = cairo_pattern_create_linear(x1+x1*1/5, y1, x2-x2*1/5, y2);
    set_button_gradient(pattern, ui);

    cairo_set_line_width(cr, 0);
    cairo_set_source(cr, pattern);
    plotNextIcon(cr, use_x1, use_y1, use_x2-use_x1, use_y2-use_y1);
    cairo_fill(cr);

    cairo_set_source_rgb(cr, 0,0,0);  // clear the pattern right away
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

#if 0
unused miscellany
    // Give the arrows a 2:3 slope
        arrow_size = (right - left) * 2 / 3;
        arrow_size = (bottom - top) * 2 / 3;

cairo_pattern_t * w_ProgressBar::makePattern ()
    {
    if ( vertical )
        return cairo_pattern_create_linear((right-left)*1/5, 0,
                                           (right-left)*4/5, arrow_size);
    else
        return cairo_pattern_create_linear(arrow_size*1/5, 0,
                                           arrow_size*4/5, bottom-top);
    }

#endif

// ---------------------------------------------------------------------- <fold<
//  -   Clickable Widgets                                                 >fold>

void w_Volume::check_size (CairoUI *ui)
    {
    squarify_small();
    }

bool w_Volume::button_press (CairoUI *ui, GenericEvent *ev)
    {
    if ( ev->button == LEFT_CLICK )
        {
        if ( mcp->muted() )
            mcp->command(COMMAND_UNMUTE);
        else
            mcp->command(COMMAND_MUTE);
        return false;
        }
    else if ( ev->button == RIGHT_CLICK )
        {
        // right click menu..
        }
    return false;
    }

bool w_Volume::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == WHEEL_UP || ev->button == WHEEL_DOWN )
        {
        int delta = 5;
        if ( ev->modifier & GenericEvent::mask.shift )
            delta = 1;
        else if ( ev->modifier & GenericEvent::mask.control )
            delta = 25;
        mcp->change_volume(ev->button == WHEEL_UP ? delta : -delta, false);
        return true;
        }
    return false;
    }

void w_Volume::draw (CairoUI *ui, cairo_t *cr)
    {
    double xunit = width/16,
           yunit = height/16;

    // First, draw the 'sound coming out of the speaker' lines
    if ( xunit + yunit < 3.01 )  // too small, just draw 2 vertical lines
        {
        cairo_set_line_width(cr, 1);
        cairo_move_to(cr, x1 + 11*xunit, y1 + 5*yunit);
        cairo_line_to(cr, x1 + 11*xunit, y2 - 5*yunit);
        cairo_move_to(cr, x1 + 14*xunit, y1 + 3*yunit);
        cairo_line_to(cr, x1 + 14*xunit, y2 - 3*yunit);
        }
    else  // draw a couple curves
        // TODO draw n curves as the size grows
        //      - just hard-coding two arcs at the corners for now
        //      - At a certain size, draw cresents
        // TODO these curves might look better as arcs
        {
        // this is a diagonal for the ends of the curves (on the top)
        double arcx1 = x1 + 10*xunit, arcy1 = y1 + 5*yunit,
               arcx2 = x1 + 14*xunit, arcy2 = y1 + 2*yunit;
        cairo_set_line_width(cr, 1);

        cairo_move_to(cr, arcx1, arcy1);
        cairo_curve_to(cr, arcx1 + 2*xunit, (y1+y2)/2,
                                  arcx1 + 2*xunit, (y1+y2)/2,
                                  arcx1, y2 - (arcy1 - y1));

        cairo_move_to(cr, arcx2, arcy2);
        cairo_curve_to(cr, arcx2 + 2*xunit, (y1+y2)/2,
                                  arcx2 + 2*xunit, (y1+y2)/2,
                                  arcx2, y2 - (arcy2 - y1));

        cairo_move_to(cr, 0, 0);
        }
    ui->button_fg.set(cr);
    cairo_stroke(cr);

    cairo_pattern_t *pattern;
    pattern = cairo_pattern_create_linear(x1, y1 + yunit, x1, y2 - yunit);
    cairo_pattern_add_color_stop_rgba(pattern, 0.0,
                                ui->button_fg1.red, ui->button_fg1.green,
                                ui->button_fg1.blue, ui->button_fg1.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 0.3,
                                ui->button_fg.red, ui->button_fg.green,
                                ui->button_fg.blue, ui->button_fg.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 0.4,
                                ui->button_fg.red, ui->button_fg.green,
                                ui->button_fg.blue, ui->button_fg.alpha);
    cairo_pattern_add_color_stop_rgba(pattern, 1.0,
                                ui->button_fg2.red, ui->button_fg2.green,
                                ui->button_fg2.blue, ui->button_fg2.alpha);
    cairo_move_to(cr, x1 + 8*xunit, y1 + 2*yunit);
    cairo_line_to(cr, x1 + 8*xunit, y2 - 2*yunit);
    cairo_line_to(cr, x1 + 2*xunit, y1 + 10*yunit);
    cairo_line_to(cr, x1,           y1 + 10*yunit);
    cairo_line_to(cr, x1,           y1 +  6*yunit);
    cairo_line_to(cr, x1 + 2*xunit, y1 +  6*yunit);
    cairo_line_to(cr, x1 + 8*xunit, y1 + 2*yunit);
    cairo_set_source(cr, pattern);
    cairo_fill(cr);
    cairo_set_source_rgb(cr, 0,0,0);  // clear the pattern right away
    cairo_pattern_destroy(pattern);
    redraw_needed = false;
    }

// ---------------------------------------------------------------------- <fold<
//  -   Text Drawing                                                      >fold>

// Returns true if a font_size >= min_size resulted <= width.
// If min_size was still too big, returns false and sets font_size to min_size.
bool w_Text::reduce_font_for_width (cairo_t *cr, double *font_size,
                                    const char *str, double min_size)
    {
    cairo_text_extents_t ext;
    cairo_set_font_size(cr, *font_size);
    cairo_text_extents(cr, str, &ext);
    if ( ext.width > width )
        {
        // This should result in the right size, but with font hinting, etc
        // it might be a little off, hence the fudge factor & loop
        // Reduce the size proportional to the width:
        //    desired width/text width  =  desired font size/font size
        //    so, font size * desired width / text width = desired font size
        double trysize = *font_size * width / ext.width;
        trysize += .25;  // fudge factor
        while ( trysize >= min_size )
            {
            cairo_set_font_size(cr, trysize);
            cairo_text_extents(cr, str, &ext);
            if ( ext.width <= width )
                {
                *font_size = trysize;
                return true;
                }
            // still a little big, nudge it down until we get there
            trysize -= .5;
            }
        }
    else
        return true;
    *font_size = min_size;
    return false;
    }

void w_Text::draw_text (CairoUI *ui, cairo_t *cr,
                        const char *text, const char *separator)
    {
    // widgets should leave CairoUI to do clipping, however draw_text
    // displays unverified text that is not known to be within the border
    // and should thus always clip.  Maybe.
    cairo_save(cr);
    cairo_set_line_width(cr, 0);
    cairo_rectangle(cr, x1, y1, width, height);
    cairo_clip(cr);

assert(cairo_status(cr) == 0);
    cairo_set_font_size(cr, fm.size);
    ui->default_fg.set(cr);
    if ( justify != JUSTIFY_CENTER || separator == 0 ||
         !text_center_on_sep(ui, cr, text, separator) )
        justify_text(ui, cr, text);
assert(cairo_status(cr) == 0);
    cairo_show_text(cr, text);
    cairo_restore(cr);
    }

void w_Text::justify_text (CairoUI *ui, cairo_t *cr, const char *text)
    {
    cairo_text_extents_t ext;
    cairo_text_extents(cr, text, &ext);
assert(cairo_status(cr) == 0);

    if ( ext.width >= width )
        text_truncate(ui, cr, text, &ext);
    else
        {
        double use_x1 = x1;
        if ( justify == JUSTIFY_LEFT )
            ;
        else if ( justify == JUSTIFY_RIGHT )
            use_x1 += width - ext.width;
        else if ( justify == JUSTIFY_CENTER )
            use_x1 += (width - ext.width) / 2;
        cairo_move_to(cr, use_x1 - ext.x_bearing, y2 - fm.y_origin);
        }
    }

// FIXME  this is not finding the actual center of the separator.
//        ?  One factor is the hinting on either side of the separator
//        ?  use the lengths of the front and back strings with and without
//           the separator to get a better width..
//           ?  other factors...
// returns false if the centered text exceeded the bounds
bool w_Text::text_center_on_sep (CairoUI *ui, cairo_t *cr, 
                                 const char *text, const char *sep)
    {
    cairo_text_extents_t ext,
                         ext2;
    double front_width,
           sep_width;
    char front[strlen(text)],
         *p;

    cairo_text_extents(cr, text, &ext);
assert(cairo_status(cr) == 0);
    if ( !sep || *sep == '\0' || !(p = strstr(text, sep)) )
        {
        // No separator found, just do a flat centering.
        justify_text(ui, cr, text);
        return ext.width > width;
        }
    strncpy(front, text, (int)(p - text));
    front[(int)(p - text)] = '\0';

    cairo_text_extents(cr, front, &ext2);
assert(cairo_status(cr) == 0);
    front_width = ext2.width;
    cairo_text_extents(cr, sep, &ext2);
assert(cairo_status(cr) == 0);
    sep_width = ext2.width;
    front_width += sep_width/2;  // not precise but hopefully close enough

    double center = (x2 + x1)/2;
    double left, right;
    if ( ext2.x_advance > 0 )
        {
        left = center - front_width;
        right = left + ext.width;
        }
    else  // TODO untested
        {
        right = center + front_width;
        left = right - ext.width;
        }
    double offset = left - x1;
    // normal: cairo_move_to(cr, x1 - ext.x_bearing, y2 - fm.y_origin);
    cairo_move_to(cr, x1 - ext.x_bearing + offset, y2 - fm.y_origin);

    // done with ext, set it to the back section of text
    cairo_text_extents(cr, p, &ext);
assert(cairo_status(cr) == 0);
    // are the front and back strings both within the widget's area
    return ( center - sep_width/2 + ext.width <= x2 &&
             center - sep_width/2 + ext2.width <= x2 );
    }

void w_Text::text_truncate (CairoUI *ui, cairo_t *cr, const char *text,
                            const cairo_text_extents_t *ext)
    {
    if ( (truncate_end && ext->x_advance > 0) ||
         (!truncate_end && ext->x_advance < 0) )
        {
        cairo_move_to(cr, x1 - ext->x_bearing, y2 - fm.y_origin);
        }
    else
        {
        double diff = ext->width - ext->width;
        cairo_move_to(cr, (x1 + diff) - ext->x_bearing, y2 - fm.y_origin);
        }
    }

#if 0
bool w_Text::text_status (CairoUI *ui, cairo_t *cr, const char *text)
    {
    cairo_status_t status = cairo_status(cr);

    if ( status != CAIRO_STATUS_SUCCESS )
        {
        mcp->log_verbose(5, "cairo: %s (%s)\n",
                         cairo_status_to_string(status), text);
        assert(status == CAIRO_STATUS_INVALID_STRING);
abort();
        ui->local->get_cairo(ui);
        return false;
        }
    return true;
    }
#endif

// ---------------------------------------------------------------------- <fold<
//  -   Position Display                                                  >fold>

w_Position::w_Position ()
    {
    slash_separator = (char *)"/";
    truncate_end = false;
    }

void w_Position::configure (CairoUI *ui)
    {
    fm.size = 0;
    }

void w_Position::event (CairoUI *ui, Event ev)
    {
    if ( ev == EVENT_POSITION )
        redraw_needed = true;
    else if ( ev == EVENT_PLAYLIST )
        {
        fm.size = 0;
        redraw_needed = true;
        }
    }

// INTL assumes horiz text
void w_Position::determine_font_size (cairo_t *cr)
    {
    char buf[20];

    fm.size = height;
    get_font_metrics(cr, &fm, "vol:");
    sprintf(buf, "%u/%u", mcp->playlist_length, mcp->playlist_length);
    if ( !reduce_font_for_width(cr, &fm.size, buf, fm.size*4/5) )
        {
        fm.size = height;
        sprintf(buf, "%u", mcp->playlist_length);
        reduce_font_for_width(cr, &fm.size, buf, fm.size*3/5);
        }
assert(!cairo_status(cr));

    if ( fm.size > height * 7/8 )
        fm.size = height * 7/8;  // fudge-factor size reduction

    get_font_metrics(cr, &fm, slash_separator);
    }

// TODO  On a mouseover, temporarily expand the widget to its full width
//       if it is currently truncated.
void w_Position::draw (CairoUI *ui, cairo_t *cr)
    {
    if ( fm.size == 0 )
        determine_font_size(cr);
assert(cairo_status(cr) == 0);
    cairo_set_font_size(cr, fm.size);
    ui->default_fg.set(cr);

    char text[20];
    sprintf(text, "%d%s%u", mcp->playlist_position+1, slash_separator,
            mcp->playlist_length);

    if ( justify == JUSTIFY_CENTER )
        {
        if ( !text_center_on_sep(ui, cr, text, slash_separator) )
            {
assert(cairo_status(cr) == 0);
            sprintf(text, "%u", mcp->playlist_position+1);
            justify_text(ui, cr, text);
assert(cairo_status(cr) == 0);
            }
        }
    else
        {
        cairo_text_extents_t ext;
        cairo_text_extents(cr, text, &ext);
assert(cairo_status(cr) == 0);
        if ( ext.width > width )
            sprintf(text, "%u", mcp->playlist_position+1);
        justify_text(ui, cr, text);
assert(cairo_status(cr) == 0);
        }
    cairo_show_text(cr, text);
/*
    if ( !text_status(ui, text) )
        {
        sprintf(text, "-%s-", slash_separator);
        cairo_show_text(cr, text);
assert(cairo_status(cr) == 0);
        }
*/

    redraw_needed = false;
    }

// ---------------------------------------------------------------------- <fold<
//  -   Time and Status Display                                           >fold>

w_TimeDisplay::w_TimeDisplay ()
    : display_type(UI::TIME), showing_text(0), showing_sep(0)
    {
    truncate_end = true;
    }

void w_TimeDisplay::configure (CairoUI *ui)
    {
    fm.size = 0;
    }

void w_TimeDisplay::timer_event (CairoUI *ui)
    {
    free(showing_text);
    showing_text = 0;
    free(showing_sep);
    showing_sep = 0;
    redraw_needed = true;
    }

void w_TimeDisplay::event (CairoUI *ui, Event ev)
    {
    switch ( ev )
        {
        case EVENT_STOPPED:
        case EVENT_PLAYTIME:
            if ( showing_text == 0 )
                redraw_needed = true;
            break;
            
        case EVENT_VOLUME:
            {
            char buf[31];
            if ( mcp->volume_left == mcp->volume_right )
                sprintf(buf, "vol:%d", mcp->volume_left);
            else
                sprintf(buf, "vol:%d,%d", mcp->volume_left, mcp->volume_right);
            show_text(buf, 2);
            }
            break;

        default:
            break;
        }
    }

void w_TimeDisplay::event (CairoUI *ui, WidgetEvent event)
    {
    switch ( event )
        {
        case WE_SELECTING_TIME:
            {
            char text[32];
            ui->time_str(text, ui->selecting_time);
            show_text(text, ui->time_separator, 2);
            break;
            }
        case WE_NOT_SELECTING_TIME:
            free(showing_text);
            showing_text = 0;
            free(showing_sep);
            showing_sep = 0;
            clear_timer();
            redraw_needed = true;
            break;

        default:
            break;
        }
    }

void w_TimeDisplay::determine_font_size (CairoUI *ui, cairo_t *cr)
    {
    char   buf[20];

    fm.size = height;
    get_font_metrics(cr, &fm, "vol:");
    sprintf(buf, "-%c%c%s%c%c%s%c%c",
            fm.widest_digit, fm.widest_digit, ui->time_separator,
            fm.widest_digit, fm.widest_digit, ui->time_separator,
            fm.widest_digit, fm.widest_digit);
    if ( !reduce_font_for_width(cr, &fm.size, buf, fm.size*4/5) )
        {
assert(cairo_status(cr) == 0);
        fm.size = height;
        sprintf(buf, "-%c%c%s%c%c",
                fm.widest_digit, fm.widest_digit, ui->time_separator,
                fm.widest_digit, fm.widest_digit);
        reduce_font_for_width(cr, &fm.size, buf, fm.size*3/5);
        }
assert(cairo_status(cr) == 0);

    if ( fm.size > height * 7/8 )
        fm.size = height * 7/8;  // fudge-factor size reduction
    get_font_metrics(cr, &fm, "vol:");
    }

bool w_TimeDisplay::button_press (CairoUI *ui, GenericEvent *ev)
    {
    if ( ev->button == LEFT_CLICK )
        {
        if ( display_type == UI::OVERALL_TIME_REVERSE )
            display_type = UI::TIME;
        else
            display_type = (UI::TimeDisplay)(display_type + 1);
        redraw_needed = true;
        return true;
        }
    else if ( ev->button == RIGHT_CLICK )
        {
        if ( display_type == UI::TIME )
            display_type = UI::OVERALL_TIME_REVERSE;
        else
            display_type = (UI::TimeDisplay)(display_type - 1);
        redraw_needed = true;
        return true;
        }

    return false;
    }

void w_TimeDisplay::show_text (const char *text, const char *separator,
                               time_t seconds)
    {
    set_timer(seconds * 1000);
    showing_text = strdup(text);
    if ( separator != 0 )
        showing_sep = strdup(separator);
    redraw_needed = true;
    }

void w_TimeDisplay::draw (CairoUI *ui, cairo_t *cr)
    {
    char text[32];
    cairo_text_extents_t ext;

    if ( fm.size == 0 )
        determine_font_size(ui, cr);

    if ( showing_text )
        {
        draw_text(ui, cr, showing_text, showing_sep);
        redraw_needed = false;
        return;
        }

    cairo_set_font_size(cr, fm.size);

    ui->playtime_str(text, display_type, 0);
    ui->default_fg.set(cr);

    cairo_text_extents(cr, text, &ext);
assert(cairo_status(cr) == 0);
    if ( justify == JUSTIFY_CENTER )  // center on the first time separator
        {
        if ( !text_center_on_sep(ui, cr, text, ui->time_separator) )
            {
assert(cairo_status(cr) == 0);
            justify_text(ui, cr, text);
assert(cairo_status(cr) == 0);
            }
//        cairo_move_to(cr, x1 + width/2 - ext.x_advance +
//                (strlen(text) < 7 ? fm.timesep_advance/2 + 2*fm.num_advance
//                                  : fm.timesep_advance + 3*fm.num_advance
//                ), y2 - fm.y_origin);
        }
    else
        justify_text(ui, cr, text);
assert(cairo_status(cr) == 0);
    cairo_show_text(cr, text);
    //if ( !text_status(ui, text) )
        //cairo_show_text(cr, "???");

    redraw_needed = false;
    }

// ---------------------------------------------------------------------- <fold<
//  -   Progress Bar                                                      >fold>

void w_ProgressBar::configure (CairoUI *ui)
    {
    left   = x1;
    right  = x2;
    top    = y1;
    bottom = y2;

    vertical = (width < (height * .75));

    linewidth = vertical ? (right - left)/5 :
                           (bottom - top)/7;
    if ( linewidth < 1.0 ) linewidth = 1.0;

    // add some border space so we can (mostly) ignore line spacing issues
    if ( vertical )
        { top += linewidth; bottom -= linewidth; }
    else
        { left += linewidth; right -= linewidth; }
    }

void w_ProgressBar::timer_event (CairoUI *ui)
    {
    ui->selecting_time = selected_time();
    ui->fire_event(WE_SELECTING_TIME);
    }

void w_ProgressBar::event (CairoUI *ui, Event ev)
    {
    if ( ev == EVENT_PLAYTIME )
        redraw_needed = true;
    }

void w_ProgressBar::select (int x, int y)
    {
    if ( vertical )
        {
        selected = y;
        if ( selected < top ) selected = top;
        if ( selected > bottom ) selected = bottom;
        }
    else
        {
        selected = x;
        if ( selected < left )  selected = left;
        if ( selected > right ) selected = right;
        }
    }

unsigned int w_ProgressBar::selected_time ()
    {
    unsigned int result;
    // newpos/songlength = x/width, newpos = songlen * x/width
    if ( vertical )
        result = (mcp->song_length * (selected - (int)top) / (bottom - top));
    else
        result = (mcp->song_length * (selected - (int)left) / (right - left));
    if ( result >= mcp->song_length )
        return ( mcp->song_length > 1000 ) ? mcp->song_length - 1000 : 0;
    else
        return result;
    }

bool w_ProgressBar::button_press (CairoUI *ui, GenericEvent *ev)
    {
    if ( ev->button == LEFT_CLICK )
        {
        pressed = true;
        select(ev->x, ev->y);
        // To avoid futzing with the TimeDisplay in the case of a quick
        // single click, use a short delay here
        set_timer(600);  // arbitrary, could query double click time?
        redraw_needed = true;
        return true;
        }
    return false;
    }

bool w_ProgressBar::button_release (CairoUI *ui, GenericEvent *ev, bool inside)
    {
    if ( ev->button == LEFT_CLICK && pressed )
        {
        pressed = false;
        clear_timer();
        ui->fire_event(WE_NOT_SELECTING_TIME);
        if ( inside )
            mcp->change_position(selected_time());
        return true;
        }
    return false;
    }

bool w_ProgressBar::pointer_motion (CairoUI *ui, GenericEvent *ev)
    {
    if ( pressed )
        {
        select(ev->x, ev->y);
        ui->selecting_time = selected_time();
        ui->fire_event(WE_SELECTING_TIME);
        redraw_needed = true;
        indicator_needed = true;
        }

    if ( mouseover )
        return false;

    mouseover = true;
    if ( !pressed )
        {
        redraw_needed = true;
        indicator_needed = true;
        }
    return true;
    } 

void w_ProgressBar::pointer_leave (CairoUI *ui)
    {
    mouseover = false;
    redraw_needed = true;
    indicator_needed = true;
    }

// TODO  In a mythical future this will quickly clear the old & draw the new
void w_ProgressBar::redraw_indicator (CairoUI *ui, cairo_t *cr)
    {
    indicator_needed = false;
    }

void w_ProgressBar::draw (CairoUI *ui, cairo_t *cr)
    {
    int center = vertical ? (left + right) / 2 : (top + bottom) / 2;

    if ( indicator_needed )
        {
        redraw_indicator(ui, cr);
        // return;
        }

    // clear the bar area
#if 0
    ui->default_bg.set(cr);
    cairo_set_line_width(cr, 0);
    if ( vertical )
        cairo_rectangle(cr, left, top - linewidth,
                            right-left, bottom-top+linewidth*2);
    else
        cairo_rectangle(cr, left - linewidth, top,
                            right-left+linewidth*2, bottom-top);
    cairo_fill(cr);
#endif

    // plot the grey bar
    cairo_set_line_width(cr, linewidth);
    cairo_set_source_rgb(cr, .3, .3, .3);
    if ( vertical )
        {
        cairo_move_to(cr, center, top);
        cairo_line_to(cr, center, bottom);
        }
    else
        {
        cairo_move_to(cr, left,  center);
        cairo_line_to(cr, right, center);
        }
    cairo_stroke(cr);

    // now plot the current position bar & indicator
    //
    // clear this song_position > 0 here; if 0, just skip the selected
    // part of the bar and look for the mouseover/pressed part.
    // Or something.
    if ( mcp->song_position > 0 && mcp->song_length > 0 )
        {
        // song_position to songlength : bar-position to bar-length
        double position;
        ui->pbar_fg.set(cr);
        if ( vertical )
            {
            position = top;
            position += mcp->song_position * (bottom - top) / mcp->song_length;
            if ( position > bottom )
                position = bottom;
            cairo_move_to(cr, center, top);
            cairo_line_to(cr, center, position);
            if ( mouseover )
                {
                cairo_stroke(cr);
                cairo_set_source_rgb(cr, .99, .99, .99);
                }
            if ( mouseover && pressed )
                position = selected;
            cairo_move_to(cr, left + linewidth/2, position);
            cairo_line_to(cr, right - linewidth/2, position);
            cairo_stroke(cr);
            }
        else
            {
            position = left;
            position += mcp->song_position * (right - left) / mcp->song_length;
            if ( position > right )
                position = right;
            cairo_move_to(cr, left, center);
            cairo_line_to(cr, position, center);
            if ( mouseover )
                {
                cairo_stroke(cr);
                cairo_set_source_rgb(cr, .99, .99, .99);
                }
            if ( mouseover && pressed )
                position = selected;
            cairo_move_to(cr, position, top + linewidth/2);
            cairo_line_to(cr, position, bottom - linewidth/2);
            cairo_stroke(cr);
            }
        }
    redraw_needed = false;
    }

// ---------------------------------------------------------------------- <fold<
//  -   Canvas Operations                                                 >fold>

w_Canvas::w_Canvas ()
    : surface(0)
    {}

void w_Canvas::draw_surface (cairo_t *cr, double use_x, double use_y, double scale)
    {
    cairo_save(cr);
    cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
    cairo_set_line_width(cr, 0);
    cairo_rectangle(cr, x1, y1, width, height);
    cairo_clip(cr);
    cairo_translate(cr, use_x, use_y);
    cairo_scale(cr, scale, scale);
    cairo_set_source_surface(cr, surface, 0, 0);
    cairo_paint(cr);
    cairo_restore(cr);
    }

TestCanvas::TestCanvas ()
    : test1(false), test2(false), pattern(0)
    {}

void TestCanvas::set_option (const char *str)
    {
    if ( strcasecmp(str, "test1") == 0 )
        test1 = true;
    else if ( strcasecmp(str, "test2") == 0 )
        test2 = true;
    }

void TestCanvas::disable ()
    {
    if ( surface )
        cairo_surface_destroy(surface), surface = 0;
    if ( pattern )
        cairo_pattern_destroy(pattern), pattern = 0;
    }

void TestCanvas::configure (CairoUI *ui)
    {
    if ( !test1 && !test2 )
        { fprintf(stderr, "TestCanvas !test1 !test2\n"); return; }

    if ( surface )
        cairo_surface_destroy(surface), surface = 0;
    if ( pattern )
        cairo_pattern_destroy(pattern), pattern = 0;

    if ( test1 )
        {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 8, 8);
        cairo_t *cr = cairo_create(surface);
        cairo_set_line_width(cr, 0.5);
        cairo_set_source_rgba(cr, .1, .3, .5, 1.0);
        cairo_move_to(cr, 2.5, 0);
        cairo_line_to(cr, 2.5, 8);
        cairo_move_to(cr, 0, 2.5);
        cairo_line_to(cr, 8, 2.5);
        cairo_stroke(cr);
        pattern = cairo_pattern_create_for_surface(surface);
        cairo_destroy(cr);
        cairo_surface_destroy(surface);
        surface = 0;
        }
    else if ( test2 )
        {
        surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 12, 12);
        cairo_t *cr = cairo_create(surface);
        cairo_set_line_width(cr, 0.5);
        cairo_set_source_rgba(cr, .2, .2, .2, 1.0);
        cairo_move_to(cr,  5.5,  0);
        cairo_line_to(cr,  5.5, 12);
        cairo_move_to(cr,  0,  5.5);
        cairo_line_to(cr, 12,  5.5);
        cairo_stroke(cr);
        pattern = cairo_pattern_create_for_surface(surface);
        cairo_destroy(cr);
        cairo_surface_destroy(surface);
        surface = 0;
        }

    cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT);
    }

bool TestCanvas::draw_background (CairoUI *ui, cairo_t *cr,
                        double d_x1, double d_y1, double d_x2, double d_y2)
    {
    if ( d_x2 < x1 || d_x1 > x2 || d_y1 > y2 || d_y2 < y1 )
        return false;
    if ( !test1 && !test2 )
        return false;
if ( pattern == 0 )
{ fprintf(stderr, "OH NOES\n"); return false; }

    cairo_save(cr);
    cairo_set_line_width(cr, 0);
    cairo_rectangle(cr, x1, y1, width, height);
    cairo_clip(cr);
    cairo_set_source(cr, pattern);
    cairo_paint(cr);
    cairo_restore(cr);
    return true;
    }

// ---------------------------------------------------------------------- <fold<
//  -   Coverart                                                          >fold>
#ifdef COVERART

// FIXME  draw centered within a rectangular size rather than enforcing a square
//        (not all album art is necessarily a square..)
w_Coverart::w_Coverart ()
    {
    min_size = 75;
    minimized = false;
    surface = 0;
    source_hash.clear();
    }

w_Coverart::~w_Coverart ()
    { clear_picture(); }

void w_Coverart::check_size (CairoUI *ui)
    {
    if ( width > height ) width = height;
    if ( height > width ) height = width;
    }

void w_Coverart::configure (CairoUI *ui)
    {
    if ( surface == 0 )
        if ( mcp->current_info != 0 &&
             mcp->current_info->id == mcp->current_id() )
            mcp->request_picture(mcp->current_id());
    }

void w_Coverart::disable ()
    {
    clear_picture();
    }

void w_Coverart::event (CairoUI *ui, Event ev, EventData *evd)
    {
    if ( ev == EVENT_PICTURE_DATA )
        {
        if ( evd->id != mcp->current_id() )
            return;
        if ( !evd->picture.data )
            {
            clear_picture();
            return;
            }
        // Showing it already?
//        if ( source_hash.compare(evd->picture.xmms_hash) == 0 )
        if ( evd->picture.xmms_hash->compare(source_hash) == 0 )
            return;

        set_picture(ui, evd->picture.data, evd->picture.length);
        // If set_picture fails, it would fail if we try the same data so
        // there's no difference between success and failure here
        source_hash.assign(evd->picture.xmms_hash->c_str());
        }
    else if ( ev == EVENT_POSITION )
        {
        // TODO  if stopped there is no ID change, so setup a timer to request
        //       the picture of this playlist entry after second or so
        }
    }

void w_Coverart::event (CairoUI *ui, Event ev)
    {
    if ( ev == EVENT_CURRENT_INFO )
        {
        // Can't request the picture right away with an ID change
        // since it might not have the hash yet.
        assert(mcp->current_info);
        if ( source_hash.compare(mcp->current_info->picture_front) != 0 )
            mcp->request_picture(mcp->current_id());
        }
    }

void w_Coverart::clear_picture ()
    {
    if ( surface )
        {
        unsigned char *data = cairo_image_surface_get_data(surface);
        cairo_surface_destroy(surface);
        free(data);
        surface = 0;
        source_hash.clear();
        redraw_needed = true;
        }
    }

// TODO  Move this (possibly slow) picture decoding to a thread out of
//       the events chain
void w_Coverart::set_picture (CairoUI *ui,
                              unsigned char *data, unsigned int length)
    {
    redraw_needed = true;
    Coverart ca(data, length);

    if ( !ca.start() )
        {
        clear_picture();
        return;
        }
// TODO  add the correct cairo version check for this.. around version 1.6?
#if 1
    ca.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, ca.width);
#else
    ca.stride = 0;  // use default
#endif
    if ( ca.finish() )
        mcp->log_verbose(10, "Coverart.set_picture: new image\n");
    else
        {
        clear_picture();
        return;
        }

    clear_picture();
    source_hash.assign(mcp->current_info->picture_front);
    surface = cairo_image_surface_create_for_data(ca.data, CAIRO_FORMAT_ARGB32,
                                                ca.width, ca.height, ca.stride);
    ca.data = 0;  // would be freed otherwise
    }

void w_Coverart::draw (CairoUI *ui, cairo_t *cr)
    {
    if ( surface )
        {
        double scale;
        double use_x = x1,
               use_y = y1;
        int    image_width  = cairo_image_surface_get_width(surface),
               image_height = cairo_image_surface_get_height(surface);
        if ( image_width < image_height )
            {
            scale = (double)height / image_height;
            use_x += ((image_height - image_width) * scale)/2;
            }
        else
            {
            scale = (double)width / image_width;
            use_y += ((image_width - image_height) * scale)/2;
            }
        mcp->log_verbose(11, "Coverart.draw: drawing surface (%lf, %lf)\n",
                         use_x, use_y);
        draw_surface(cr, use_x, use_y, scale);
        }
    else
        ;  // TODO draw the logo?  Maybe a dancing baby?

    redraw_needed = false;
    }

bool w_Coverart::button_press (CairoUI *ui, GenericEvent *ev)
    {
    mcp->write_config();
    return false;
    }

#endif

// ---------------------------------------------------------------------- <fold<
//  -   Icon                                                              >fold>
// TODO  Generalize the rounded border sizing into a separate function.
 
void w_Icon::check_size (CairoUI *ui)
    {
    squarify_small();
    }

// FIXME  figure out why the pre-clipped region doesn't work here
void w_Icon::draw (CairoUI *ui, cairo_t *cr)
    {
    cairo_save(cr);
    cairo_reset_clip(cr);
    cairo_translate(cr, x1, y1);
    cairo_scale(cr, width/100.0, height/100.0);
    ui->draw_icon(cr);
    cairo_restore(cr);
    redraw_needed = false;
    }

bool w_Icon::button_press (CairoUI *ui, GenericEvent *ev)
    {
    mcp->command(COMMAND_CONNECT);
    return false;  // must be false: the control may not be present later
    }

// ---------------------------------------------------------------------- <fold<

Widget * new_widget_by_index (WidgetIndex index)
    {
    switch ( index )
        {
        case WI_STOP:        return new w_Stop();
        case WI_PLAY:        return new w_Play();
        case WI_PAUSE:       return new w_Pause();
        case WI_PLAYPAUSE:   return new w_PlayPause();
        case WI_PREVIOUS:    return new w_Previous();
        case WI_NEXT:        return new w_Next();
        case WI_VOLUME:      return new w_Volume();
        case WI_POSITION:    return new w_Position();
        case WI_TIMEDISPLAY: return new w_TimeDisplay();
        case WI_PROGRESSBAR: return new w_ProgressBar();
        case WI_ICON:        return new w_Icon();
        case WI_COVERART:    return new w_Coverart();
        case WI_TEST_CANVAS: return new TestCanvas();
//        case WI_VISUAL:    return new w_Visual()
        default: assert(2 + 2 == 5); return 0;
        }
    }

} // namespace cui
